﻿using System;
using System.Collections.Generic;
using System.ComponentModel;

namespace Notoric.ViewModel
{
    /// <summary>
    /// Common abstract base class for virtual view-model elements.
    /// </summary>
    /// <remarks>
    /// <para>
    /// A virtual view-model element is such an element that does not adapt
    /// any particular model element. Typically it adapts a single property of a model element.
    /// </para>
    /// <para>
    /// A good example is a staff line. No staff line class exists in the model,
    /// because it is unnecessary. The only information needed by the model is
    /// the number of staff lines. However, the view needs to know how to draw
    /// each particular staff line and that's why the view-model staff element
    /// owns instances of a staff line class each one filled with the coordinates
    /// of the corresponding line.
    /// </para>
    /// </remarks>
    public abstract class VirtualViewModelElement : INotifyPropertyChanged, IDisposable
    {
        #region Constructors

        /// <summary>
        /// Creates a new <see cref="VirtualViewModelElement"/> instance.
        /// </summary>
        /// <param name="owner">
        /// <para>
        /// Type: <see cref="ViewModelElement"/>
        /// </para>
        /// <para>
        /// The view-model element that directly owns this virtual view-model element.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="owner"/> is <see langword="null"/>.
        /// </exception>
        protected VirtualViewModelElement(ViewModelElement owner)
        {
            if (owner == null)
                throw new ArgumentNullException("owner");

            this.owner = owner;
            monitoredOwners = new List<ViewModelElement>();
        }

        #endregion

        #region INotifyPropertyChanged Members

        /// <inheritdoc/>
        public event PropertyChangedEventHandler PropertyChanged;

        #endregion

        #region IDisposable Members

        /// <inheritdoc/>
        public virtual void Dispose()
        {
        }

        #endregion

        #region Event Handlers

        /// <summary>
        /// Occurs when the direct or indirect owner of this element has changed.
        /// </summary>
        /// <remarks>
        /// <b>Operation:</b><br/>
        /// <para>
        /// The handler rebinds all the event listeners bound to elements that are above
        /// the changed owner.
        /// </para>
        /// <para>
        /// <i>Note:</i><br/>
        /// In reality the handler rebinds all the event listeners, because the implementation
        /// is much easier. But semantically it corresponds to what is written in the paragraph above.
        /// </para>
        /// </remarks>
        /// <param name="sender">Object that has sent the event</param>
        /// <param name="e">Event data</param>
        private void Owner_OwnerChanged(object sender, OwnerChangedEventArgs e)
        {
            for (int i = 0; i < monitoredOwners.Count; ++i)
            {
                var owner = monitoredOwners[i];

                owner.PropertyChanged -= owner_PropertyChanged;

                var el = this.owner;
                while (el != null && !(el.GetType() == owner.GetType()))
                    el = el.Owner;

                if (el != null)
                {
                    el.PropertyChanged += new PropertyChangedEventHandler(owner_PropertyChanged);
                    monitoredOwners[i] = el;
                }
            }
        }

        /// <summary>
        /// Occurs when the value of some property of a monitored owner has changed.
        /// </summary>
        /// <remarks>
        /// <b>Operation:</b><br/>
        /// The handler invokes the <see cref="Owner_PropertyChanged"/> virtual method and passes it the owner's instance
        /// and the event data.
        /// </remarks>
        /// <param name="sender">Object that has sent the event</param>
        /// <param name="e">Event data</param>
        private void owner_PropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            Owner_PropertyChanged((ViewModelElement)sender, e);
        }

        #endregion

        #region Protected Methods

        /// <summary>
        /// This method is called when some property of some of the monitored owners has changed.
        /// </summary>
        /// <remarks>
        /// This method is called in reaction to <see cref="ViewModelElement.PropertyChanged"/> event
        /// that was raised by an element that directly or indirectly owns this element and was previously
        /// registered to monitoring by a call to <see cref="RegisterToOwnerPropertyChanged{T}"/> method.
        /// The derived classes can override it to react to this event.
        /// </remarks>
        /// <param name="owner">
        /// <para>
        /// Type: <see cref="ViewModelElement"/>
        /// </para>
        /// <para>
        /// The element (owner) that has raised the event.
        /// </para>
        /// </param>
        /// <param name="e">
        /// <para>
        /// Type: <see cref="PropertyChangedEventArgs"/>
        /// </para>
        /// <para>
        /// The data of the property changed event.
        /// </para>
        /// </param>
        protected virtual void Owner_PropertyChanged(ViewModelElement owner, PropertyChangedEventArgs e)
        {
        }

        /// <summary>
        /// Raises the <see cref="PropertyChanged"/> event on this <see cref="VirtualViewModelElement"/> instance.
        /// </summary>
        /// <param name="propertyName">
        /// <para>
        /// Type: <see cref="String"/>
        /// </para>
        /// <para>
        /// The name of the property whose value has changed.
        /// </para>
        /// </param>
        protected void RaisePropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }

        #endregion

        #region Protected Properties

        /// <summary>
        /// Gets the value converter that converts between the brushes used in the model and in the view.
        /// </summary>
        protected static readonly Converters.BrushConverter BrushConverter = new Converters.BrushConverter();

        /// <summary>
        /// Gets the value converter that converts between the length units used in the model and in the view.
        /// </summary>
        protected static readonly Converters.LengthConverter LengthConverter = new Converters.LengthConverter();

        /// <summary>
        /// Gets the value converter that converts between the structures used in the model an in the view.
        /// </summary>
        protected static readonly Converters.StructuresConverter StructuresConverter = new Converters.StructuresConverter();

        #endregion

        #region Public Methods

        /// <summary>
        /// Finds a view-model element that owns (directly or indirectly) this element and has a given type.
        /// </summary>
        /// <typeparam name="T">The type of the searched owner (must be a subtype of <see cref="ViewModelElement"/></typeparam>
        /// <returns>
        /// <para>
        /// Type: <typeparamref name="T"/>
        /// </para>
        /// <para>
        /// A reference to the searched owner or <see langword="null"/> if no such owner exists.
        /// </para>
        /// </returns>
        public T FindOwner<T>() where T : ViewModelElement
        {
            var el = owner;
            if (el is T)
                return (T)el;

            return el.FindOwner<T>();
        }

        /// <summary>
        /// Registers an event listener to the <see cref="ViewModelElement.PropertyChanged"/> event
        /// of a direct or indirect owner of this element that has a given type.
        /// </summary>
        /// <remarks>
        /// <para>
        /// This method ensures the correct listener rebinding in a case when this element is migrated
        /// to a different owner.
        /// </para>
        /// <para>
        /// The derived class can override the <see cref="Owner_PropertyChanged"/> method to react
        /// to the <see cref="ViewModelElement.PropertyChanged"/> event of the registered owner.
        /// </para>
        /// </remarks>
        /// <typeparam name="T">The type of the owner to which the element wants to bind.</typeparam>
        public void RegisterToOwnerPropertyChanged<T>() where T : ViewModelElement
        {
            var owner = FindOwner<T>();

            if (owner != null && !monitoredOwners.Contains(owner))
            {
                owner.PropertyChanged += new PropertyChangedEventHandler(owner_PropertyChanged);
                monitoredOwners.Add(owner);
            }
        }

        /// <summary>
        /// Unregisters an event listener from the <see cref="ViewModelElement.PropertyChanged"/> event
        /// of a direct or indirect owner of this element that has a given type.
        /// </summary>
        /// <typeparam name="T">The type of the owner from which the element wants to unbind.</typeparam>
        /// <seealso cref="RegisterToOwnerPropertyChanged"/>
        public void UnregisterFromOwnerPropertyChanged<T>() where T : ViewModelElement
        {
            var owner = monitoredOwners.Find(o => o is T);
            if (owner != null)
            {
                owner.PropertyChanged -= owner_PropertyChanged;
                monitoredOwners.Remove(owner);
            }
        }

        #endregion

        #region Public Properties

        /// <summary>
        /// Gets the view-model element that directly owns this virtual view-model element.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="ViewModelElement"/>
        /// </para>
        /// <para>
        /// The view-model element that directly owns this virtual view-model element.
        /// </para>
        /// </value>
        public ViewModelElement Owner
        {
            get { return owner; }
        }

        #endregion

        #region Private Fields

        /// <summary>
        /// Holds the list of owners of this element whose <see cref="ViewModelElement.PropertyChanged"/> event is monitored.
        /// </summary>
        private List<ViewModelElement> monitoredOwners;
        /// <summary>
        /// The element that directly owns this virtual element.
        /// </summary>
        private ViewModelElement owner;

        #endregion
    }
}
